package com.hibegin.http.server.impl;

import com.hibegin.http.server.SimpleWebServer;
import com.hibegin.http.server.config.ResponseConfig;
import com.hibegin.http.server.util.FreeMarkerUtil;
import com.hibegin.http.server.util.GzipCompressingInputStream;
import com.hibegin.http.server.util.MimeTypeUtil;
import com.hibegin.http.server.util.ServerInfo;
import com.hibegin.http.server.util.StatusCodeUtil;
import com.hibegin.http.server.util.StringsUtil;
import com.hibegin.http.server.web.cookie.Cookie;
import com.szwistar.common.ERR;
import com.szwistar.common.datastruct.Utils;
import com.szwistar.common.os.FileUtils;
import com.szwistar.common.os.PathUtils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.zip.GZIPOutputStream;

public class HttpResponse {
	protected static final Logger LOG = LoggerFactory.getLogger(HttpResponse.class);

    protected static final String CRLF = "\r\n";
    protected static final int WRITE_BUFFER_SIZE = 32*1024;
    protected static final String SERVER_INFO = ServerInfo.getName() + "/" + ServerInfo.getVersion();

    protected SimpleWebServer server;
    protected SocketChannel channel;

    protected HttpRequest request;
    protected ResponseConfig config;
    protected long sent = 0;

    protected Map<String, String> header = new HashMap<String, String>();
    protected List<Cookie> cookieList = new ArrayList<Cookie>();

    @Override
	public String toString() {
		return "HttpResponse [sent=" + sent + "]";
	}

	public HttpResponse(SimpleWebServer server, SocketChannel channel, HttpRequest request) {
    	this.server = server;
    	this.channel = channel;
        this.request = request;
        this.config = server.getResponseConfig();

        // 初始化响应头
        initHeader();
    }

	// 初始化响应头
    private void initHeader() {
        // 服务器标识
        header.put("Server", SERVER_INFO);

        // Connection 是否需要 keep-alive
        keepAlive(request.keepAlive());
    }

    public boolean keepAlive() {
		return "keep-alive".equalsIgnoreCase(header.get("Connection"));
	}

	public void keepAlive(boolean keepAlive) {
		header.put("Connection", keepAlive ? "keep-alive" : "close");
	}

	/**
     * 不区分大小写地获取头部字段
     */
    public String getHeader(String key) {
        String headerValue = header.get(key);
        if(headerValue != null) { return headerValue; }

        for(String key0 : header.keySet()) {
            if(key0.equalsIgnoreCase(key)) {
                return header.get(key0);
            }
        }
        return null;
    }

    public void addHeader(String name, String value) {
        header.put(name, value);
    }

    public void setContentLength(Long contentLength) {
    	if(contentLength != null && contentLength >= 0) {
    		header.put("Content-Length", Long.toString(contentLength));
    	} else {
    		header.remove("Content-Length");
    	}
    }

    private String getContentType() {
		return header.get("Content-Type");
	}

    public void setContentType(String contentType) {
		if(Utils.isNotEmpty(contentType)) {
			header.put("Content-Type", contentType);
		} else {
    		header.remove("Content-Type");
		}
    }

    public void addCookie(Cookie cookie) {
        cookieList.add(cookie);
    }

    private byte[] wrapHeader(int statusCode) {
        StringBuilder sb = new StringBuilder();

        // 生成第一行
        sb.append("HTTP/1.1 ").append(statusCode).append(" ").append(StatusCodeUtil.getStatusCodeDesc(statusCode)).append(CRLF);

        // 生成其它行
        for(Entry<String, String> he : header.entrySet()) {
            sb.append(he.getKey()).append(": ").append(he.getValue()).append(CRLF);
        }

        // 处理 Cookie
        if (!config.isDisableCookie()) {
            Cookie[] cookies = request.getCookies();
            if (cookies != null) {
                for (Cookie cookie : cookies) {
                    if (cookie != null && cookie.isCreate()) {
                        cookieList.add(cookie);
                    }
                }
            }
            for (Cookie cookie : cookieList) {
                sb.append("Set-Cookie: ").append(cookie).append(CRLF);
            }
        }

        sb.append(CRLF);
        return sb.toString().getBytes();
    }

    private byte[] compress(byte[] bytes) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream(bytes.length);
        GZIPOutputStream gzip = new GZIPOutputStream(bos);
        gzip.write(bytes);
        gzip.close();
        byte[] compressed = bos.toByteArray();
        bos.close();
        return compressed;
    }

    /**
     * 根据客户端gzip兼容性、服务器gzip配置、Content-Type 和 body 长度，判断是否值得压缩？
     * @param bodyLength Body 数据长度；设置为0，可强制不压缩
     * @return 是否值得压缩。如果值得压缩，则设置压缩编码相关的头属性；否则去掉压缩编码相关的头属性
     */
    private boolean compressible(long bodyLength) {
        // 客户端是否可接受压缩编码
        String reqenc = request.getHeader("Accept-Encoding");

        boolean compressible = config.isSupportGzip() &&			// 客户端、服务器双方是否都支持 gzip
        		(reqenc != null && reqenc.indexOf("gzip") >= 0) &&	// 客户端是否可接受压缩编码
        		bodyLength > WRITE_BUFFER_SIZE &&					// 数据长度是否大于最小长度
        		MimeTypeUtil.compressible(getContentType());		// Content-Type 指示的 MIME 类型是否值得压缩

        // 如果可压缩，则采用压缩编码
        if(compressible) {
			// 使用 Gzip 压缩传输时，不能指定 Content-Length
			header.put("Content-Encoding", "gzip");
            header.remove("Content-Length");
            LOG.debug("使用压缩编码！{}", request);
		} else {
			header.remove("Content-Encoding");
		}

		return compressible;
    }

    private void send(byte[] bytes, int offset, int length) {
    	if(Utils.isEmpty(bytes)) { return; }
        try {
			sent += server.write(channel, ByteBuffer.wrap(bytes, offset, length));
		} catch (IOException e) {
			LOG.error("发送数据失败！", e);
		}
    }

    private void send(byte[] bytes) {
    	if(Utils.isEmpty(bytes)) { return; }
    	send(bytes, 0, bytes.length);
    }

    /**
     * 根据文件扩展名，渲染内容
     * @param code HTTP 状态码
     * @param ext 文件扩展名
     * @param body 内容
     */
    public void renderExt(ERR code, String ext, String body) {
		// 设置内容类型(包括编码)
    	String contentType = MimeTypeUtil.getContentType(ext);
		setContentType(contentType);

		// 内容数据
		byte[] bodyBytes = null;

    	if(Utils.isNotEmpty(body)) {
    		try {
                // 获取内容编码
                String charset = MimeTypeUtil.getCharsetExt(ext);
                if(charset == null) { charset = config.getCharSet(); }
        		// 把 body 转换成 字节数组
        		bodyBytes = body.getBytes(charset);
    		} catch (UnsupportedEncodingException e) {
    			renderException(e, "Content-Type 指定的字符编码不可用！" + contentType);
    		}

        	if(compressible(bodyBytes.length)) {
        		try {
                	// 压缩编码 body 数据
        			bodyBytes = compress(bodyBytes);
    			} catch (IOException e) {
    				LOG.error("压缩数据失败，不压缩传输了！");
    				compressible(0);
    			}
        	} else {
            	// 设置 Content-Length
        		setContentLength((long) bodyBytes.length);
        	}
    	}

    	// 发送头部数据
    	send(wrapHeader(code.code()));
    	// 发送 body 数据
    	send(bodyBytes);
    }

    public void renderJson(Object obj) {
		try {
			String json = server.getServerConfig().getHttpJsonMessageConverter().toJson(obj);
			renderExt(ERR.HTTP_OK, "json", json);
		} catch (Exception e) {
			renderException(e, "JSON 编码失败：" + obj.toString());
		}
    }

    public void renderHtml(String html) {
    	renderExt(ERR.HTTP_OK, "html", html);
    }

    public void renderText(String text) {
    	renderExt(ERR.HTTP_OK, "text", text);
    }

    public void renderCode(ERR code, String message) {
    	if(message == null) { message = code.msg(); }
        String content = StringsUtil.getHtmlStrByStatusCode(code.code(), message);
        renderHtml(content);
    }

    public void renderCode(ERR code) {
    	renderCode(code, null);
    }

    public void renderException(Exception e, String message) {
    	if(message == null) { message = ERR.SYS.msg(); }
        renderCode(ERR.SYS, message + "\r\n" + e.getMessage());
    }

    /**
     * 重定向到到指定的 URL，可以是其它主机地址
     * @param url 完整 URL
     */
    public void renderRedirect(String url) {
        header.put("Location", url);
        renderCode(ERR.HTTP_TEMPORARY_REDIRECT);
    }

    /**
     * 重定向到本机的其它资源路径
     * @param uri 本机的其它资源路径
     */
    public void renderForward(String uri) {
    	renderRedirect(request.getScheme() + "://" + request.getHeader("Host") + "/" + uri);
    }

    public void renderFreeMarker(String name) {
		try {
			String html = FreeMarkerUtil.renderToFM(name, request);
	        renderHtml(html);
		} catch (Exception e) {
			renderException(e, "FreeMarker 解析失败：" + name);
		}
    }

    public void renderFile(File file, boolean headOnly) {
        if(!file.exists()) {
            renderCode(ERR.NOT_FOUND, file.getAbsolutePath());
            return;
        }
        if(file.isDirectory()) {
            renderCode(ERR.NOT_FOUND, "文件已经存在,并且是目录！" + file.getAbsolutePath());
            return;
        }

        FileInputStream ins = null;
        try {
            // 根据文件扩展名对应的 MINE 类型，设置响应 Content-Type
            String contentType = MimeTypeUtil.getContentType(PathUtils.getFileExtension(file.getPath()));
            // 打开文件输入流
            ins = new FileInputStream(file);
            // 发送流数据
            renderStream(ERR.HTTP_OK, contentType, ins, file.length(), headOnly);
        }
        catch (Exception e) {
            LOG.error("", e);
        }
        finally {
        	FileUtils.close(ins);
        }
    }

    public void renderFileAttachment(File file, boolean headOnly) {
        header.put("Content-Disposition", "attachment;filename=\"" + file.getName() + "\"");
        renderFile(file, headOnly);
    }

    /**
     * 发送流式响应
     * @param errCode 状态码
     * @param contentType 内容类型指示
     * @param ins 输入流
     * @param contentLength 内容长度指示；如果为 null，则尝试从 ins 中判断长度；如果 ins 不支持长度判断，则采用 {@link #renderChunkedStream()} 方式传输
     * @param headOnly 是否只提供头部信息(HEAD 方法响应时)
     */
    public void renderStream(ERR errCode, String contentType, InputStream ins, Long contentLength, boolean headOnly) {
        try {
            // 设置响应 Content-Type
            setContentType(contentType);

        	// 计算内容长度
            if(contentLength == null || contentLength <= 0) {
            	try {
            		contentLength = (long) ins.available();
				} catch (Exception e) {
					// 无法确定流中的数据长度？
				}
            }

            // 如果无法确定流的长度，则只能采用 Chunked 编码发送
            // 如果需要使用压缩传输，则最好采用 Chunked 编码发送；否则就只能在发送完成之后关闭连接，以指示传输结束
            if(contentLength == null || contentLength <= 0 || compressible(contentLength)) {
            	renderStreamChunked(errCode, contentType, ins, headOnly);
            	return;
            }

        	// 不使用 gzip 时，要设置内容长度！
        	setContentLength(contentLength);

        	// 先发送响应头（包括内容长度字段）
            send(wrapHeader(errCode.code()));

            // 如果需要数据，才发送数据
            if(!headOnly) {
                // 一块一块地发送
                int length = 0;
            	byte[] tempBytes = new byte[WRITE_BUFFER_SIZE];
                while((length = ins.read(tempBytes)) != -1) {
                    send(tempBytes, 0, length);
                }
            }
        }
        catch (Exception e) {
            LOG.error("", e);
        }
        finally {
        	FileUtils.close(ins);
        }
    }

    /**
     * 使用 Chunked 编码方式发送流式响应
     * <p>如果无法确定流的长度，则只能采用 Chunked 编码发送
     * @param errCode 状态码
     * @param contentType 内容类型指示
     * @param ins 输入流
     * @param headOnly 是否只提供头部信息(HEAD 方法响应时)
     */
    public void renderStreamChunked(ERR errCode, String contentType, InputStream ins, boolean headOnly) {
        if (ins == null) {
            renderCode(ERR.NOT_FOUND); return;
        }

        // 数据输入流
        InputStream dataIns = null;

        try {
        	// 发送头
            header.remove("Content-Length");
            header.put("Transfer-Encoding", "chunked");
            send(wrapHeader(errCode.code()));

            if(!headOnly) {
	            if(compressible(WRITE_BUFFER_SIZE+1)) {
		            // 如果需要压缩，则要把输入流包装为 Gzip 压缩输入流
	            	dataIns = new GzipCompressingInputStream(ins);
	            } else {
	            	// 不需要压缩
	            	dataIns = ins;
	            }

	            // 一个 Chunk 一个 Chunk 地发送
	            int length = 0;
	            byte[] bytes = new byte[WRITE_BUFFER_SIZE];
	            while((length = dataIns.read(bytes)) != -1) {
	            	sendChunk(bytes, 0, length);
	            }

	            // Chunk 发送结束时，必须发送一个空的 Chunk
	            sendChunk(null, 0, 0);
            }
        }
        catch (Exception e) {
            LOG.error("", e);
        }
        finally {
        	FileUtils.close(dataIns);
        	FileUtils.close(ins);
        }
    }

    /**
     * 发送一个 Chunk
     * @param chunk 数据块
     * @param offset 数据偏移
     * @param length 数据长度
     */
    private void sendChunk(byte[] chunk, int offset, int length) throws Exception {
    	// 发送 Chunk 长度指示
    	String chunkLength = Integer.toHexString(length) + CRLF;
    	send(chunkLength.getBytes());

    	// 发送 Chunk 数据
    	if(length > 0) {
    		send(chunk, offset, length);
    	}
    	// 发送 \r\n
    	send(CRLF.getBytes());
    }
}
